package com.hup.utils.commons;


import com.hup.utils.commons.io.FileUtil;
import com.sun.jna.Library;
import com.sun.jna.Native;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Date;

/**
 * 使用JNI或JNA时,dll必须是系统可以直接访问的本地文件(不能在jar内部),所以需要将dll提取出来;
 * 一般做法是提取到临时目录,本类是提取到 {@link #LIBS_DIR};
 *
 * @author hugan
 * @date 2020/4/22
 */
@Log4j2
public class JniUtil {
    private static final String LIBS_DIR = "libs";

    /**
     * 将dll提取到程序文件夹下的[ {@link #LIBS_DIR} ]目录;
     * 并且创建说明文件;
     *
     * @param libPath 在resources下的路径,含文件扩展名;如[/dll/JniMouseUtil.dll]
     * @param useResourceDesc true-使用dll同目录下的同名txt说明文件(如[JniMouseUtil.dll.txt]),false-使用 desc作为说明文件的内容
     * @param desc dll的说明内容,nullable
     */
    public static File tryCreateDll(String libPath, boolean useResourceDesc, String desc) throws IOException {
        InputStream ips = JniUtil.class.getResourceAsStream(libPath);
        if (ips == null) throw new IllegalArgumentException("找不到文件:" + libPath);

        int indexOf = libPath.lastIndexOf('/');
        String libName = indexOf != -1 ? libPath.substring(indexOf + 1) : libPath;
        log.debug("libName={}", libName);

        String appDir = System.getProperty("user.dir");
        log.debug("appDir={}", appDir);
        File dllFile = new File(appDir + "/" + LIBS_DIR + "/" + libName);
        int writeRes = FileUtil.writeIfNotSame(dllFile, ips);
        boolean writeDesc = false;
        if (writeRes == 0) {
            log.debug("[{}]文件已存在", libName);
        } else if (writeRes == -1) {
            log.warn("[{}]文件MD5不一致?,已恢复dll", libName);
            writeDesc = true;
        } else {
            writeDesc = true;
            log.info("已提取[{}]到[./{}]", libPath, LIBS_DIR);
        }
        if (writeDesc) {
            if (useResourceDesc) {
                ips = JniUtil.class.getResourceAsStream(libPath + ".txt");
                if (ips == null) throw new IllegalArgumentException("找不到文件:" + libPath + ".txt");
                try {
                    desc = IOUtils.toString(ips, StandardCharsets.UTF_8);
                } finally {
                    IOUtils.closeQuietly(ips);
                }
            }
            File descFile = new File(appDir + "/" + LIBS_DIR + "/" + libName + "说明.txt");
            try (FileOutputStream ops = new FileOutputStream(descFile)) {
                if (desc != null) ops.write(desc.getBytes());
                ops.write(("\n提取时间:" + DateBuilder.ymdHms(new Date())).getBytes());
            }
        }
        return dllFile;
    }

    /**
     * 使用 {@link System#load(String)}加载JNI类型的dll
     *
     * @param libPath 在resources下的路径,含文件扩展名;如[/dll/JniMouseUtil.dll]
     * @apiNote 需要dll同目录下, 存在同名txt说明文件(如[JniMouseUtil.dll.txt]); {@link #tryCreateDll(String, boolean, String)}
     */
    public static void loadLibraryJni(String libPath) {
        try {
            File dllFile = tryCreateDll(libPath, true, null);
            System.load(dllFile.getAbsolutePath());
        } catch (IOException e) {
            throw new IllegalArgumentException("[" + libPath + "]加载失败:" + e.getMessage());
        }
    }

    /**
     * 使用 {@link System#load(String)}加载JNI类型的dll
     *
     * @param libPath 在resources下的路径,含文件扩展名;如[/dll/JniMouseUtil.dll]
     * @param desc dll的说明内容,nullable
     */
    public static void loadLibraryJni(String libPath, String desc) {
        try {
            File dllFile = tryCreateDll(libPath, false, desc);
            System.load(dllFile.getAbsolutePath());
        } catch (IOException e) {
            throw new IllegalArgumentException("[" + libPath + "]加载失败:" + e.getMessage());
        }
    }

    /**
     * 加载JNA类型的dll;
     *
     * @param libPath 在resources下的路径,含文件扩展名;如[/dll/JniMouseUtil.dll]
     * @apiNote 需要dll同目录下, 存在同名txt说明文件(如[JniMouseUtil.dll.txt]); {@link #tryCreateDll(String, boolean, String)}
     */
    public static <T extends Library> T loadLibraryJNA(String libPath, Class<T> clz) {
        try {
            new JnaPathSetter();//仅触发一次,设置jna的lib路径
            File dllFile = tryCreateDll(libPath, true, null);
            return Native.load(dllFile.getName(), clz);
        } catch (IOException e) {
            throw new IllegalArgumentException("[" + libPath + "]加载失败:" + e.getMessage());
        }
    }

    /**
     * 加载JNA类型的dll;
     *
     * @param libPath 在resources下的路径,含文件扩展名;如[/dll/JniMouseUtil.dll]
     * @param desc dll的说明内容,nullable
     */
    public static <T extends Library> T loadLibraryJNA(String libPath, Class<T> clz, String desc) {
        try {
            new JnaPathSetter();//仅触发一次,设置jna的lib路径
            File dllFile = tryCreateDll(libPath, false, desc);
            return Native.load(dllFile.getName(), clz);
        } catch (IOException e) {
            throw new IllegalArgumentException("[" + libPath + "]加载失败:" + e.getMessage());
        }
    }

    /**
     * 设置JNA的dll目录的绝对路径
     */
    private static class JnaPathSetter {
        static {
            String appDir = System.getProperty("user.dir");
            File libDir = new File(appDir + File.separator + LIBS_DIR);
            log.debug("jna.library.path={}", libDir);
            System.setProperty("jna.library.path", libDir.getAbsolutePath());
        }
    }
}
